﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Formatters.Binary;
using System.ServiceModel;
using System.Text;
using System.Threading.Tasks;
using dis.sys.hw1.Contract;
using dis.sys.hw1.Model;
using ZooKeeperNet;
using Org.Apache.Zookeeper.Data;
using System.Runtime.Serialization;
using System.ServiceModel.Description;
using dis.sys.hw1.ClientProxy;
using System.Net;
using System.Net.Sockets;

namespace dis.sys.hw1.Service
{
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
    public class HotelService : IHotelService, IDelegateHotelService
    {
        private readonly ServiceLog _log = new ServiceLog(typeof(HotelService), true);

        public IDictionary<string, Hotel> Data { get; set; }
        public IDictionary<string, Hotel> BackedUpData { get; set; }

        public ZooKeeper Replicator { get; set; }

        public string Alliance { get; set; }
        public string SearchServiceAddress { get; set; }
        public HotelRegistrationProxy RegistrationProxy { get; set; }

        private Replication.ServerData _serverData { get; set; }
        public Replication.ServerData ServerData 
        {
            get
            {
                if (_serverData == null) return null;
                _serverData.ActiveCount = Data.Count();
                _serverData.BackupCount = BackedUpData.Count();
                return _serverData;
            }
            set
            {
                _serverData = value;
            }
        }

        public int InternalPort { get; set; }
        public int DelegatePort { get; set; }

        public HotelService(string dataFileName, int delegatePort, int internalPort, ZooKeeper replicator, string searchServiceAddress)
        {
            Replicator = replicator;
            SearchServiceAddress = searchServiceAddress;
            InternalPort = internalPort;
            DelegatePort = delegatePort;
            Data = new ConcurrentDictionary<string, Hotel>();
            BackedUpData = new ConcurrentDictionary<string, Hotel>();
            var hotel = LoadData(dataFileName);
            Alliance = hotel.Alliance;
            ServerData = new Replication.ServerData()
            {
                MainHotelName = hotel.Name,
                Address = "http://" + LocalIPAddress() + ":" + InternalPort + "/Services/HotelService",
                ActiveCount = Data.Count(),
                BackupCount = BackedUpData.Count()
            };
            var httpEndpoint = new ServiceEndpoint(ContractDescription.GetContract(typeof(IRegistrationService)),
                new BasicHttpBinding(), new EndpointAddress((SearchServiceAddress.StartsWith("http://") ? "" : "http://") + SearchServiceAddress));
            RegistrationProxy = new HotelRegistrationProxy(httpEndpoint);
            InvalidateSearchServiceCacheLines(hotel);
        }

        private void InvalidateSearchServiceCacheLines(Hotel hotel)
        {
            RegistrationProxy.InvalidateCacheLine(hotel.Name, hotel.Stars, hotel.Rating);
        }

        public void Init()
        {

            RegisterToZooKeeper();
            BackupData();
        }

        private static string LocalIPAddress()
        {
            IPHostEntry host;
            string localIP = "";
            host = Dns.GetHostEntry(Dns.GetHostName());
            foreach (IPAddress ip in host.AddressList)
            {
                if (ip.AddressFamily == AddressFamily.InterNetwork)
                {
                    localIP = ip.ToString();
                }
            }
            return localIP;
        }

        public void BackupData(Hotel hotel = null)
        {
            _log.Debug("Backing up data hotel " + (hotel == null ? "ALL" : hotel.Name));
            foreach (var server in GetOtherActiveServers().OrderBy(s => s.ActiveCount + s.BackupCount))
            {
                if (BackupDataOnServer(server.Address,hotel))
                {
                    Replicator.GetData
                    (
                        string.Format(Replication.HotelName, Alliance, server.MainHotelName),
                        new WatchMyBackup(this),
                        new Stat()
                    );
                    _log.Debug("Backed up data in " + server.Address);
                    return;
                }
            }
            WaitForBackup();
        }

        private void WaitForBackup()
        {
            _log.Debug("WaitForBackupWatch added for path: " + string.Format(Replication.Hotels, Alliance));
            Replicator.GetChildren(string.Format(Replication.Hotels, Alliance), new WaitForBackupWatch(this));
        }

        public IEnumerable<Replication.ServerData> GetOtherActiveServers()
        {
            var hotelsPath = string.Format(Replication.Hotels, Alliance);
            var servers = new List<Replication.ServerData>();
            foreach (string hotelName in Replicator.GetChildren(hotelsPath, false))
            {
                var currData = Replicator.GetData
                    (
                        string.Format(Replication.HotelName, Alliance, hotelName),
                        false,
                        new Stat()
                    ).ByteArrayToObject<Replication.ServerData>();
                if (!currData.Address.Equals(ServerData.Address) 
                    && currData.MainHotelName.Equals(hotelName))
                {
                    servers.Add(currData);
                }
            }
            return servers;
        }

        private bool BackupDataOnServer(string address, Hotel hotel = null)
        {
            try
            {
                var proxy = GenerateHotelServiceProxy(address);
                if (hotel != null)
                {
                    var backup = new Dictionary<string, Hotel>();
                    backup[hotel.Name] = hotel;
                    return proxy.AddHotelToBackUp(backup);
                }
                else
                {
                    return proxy.AddHotelToBackUp(Data);
                }
            }
            catch (Exception e)
            {
                _log.Error("BackupDataOnServer - " + e.Message);
                return false;
            }
        }

        public HotelServiceProxy GenerateHotelServiceProxy(string address)
        {
            var httpEndpoint = new ServiceEndpoint(ContractDescription.GetContract(typeof(IHotelService)),
                    new BasicHttpBinding(), new EndpointAddress((address.StartsWith("http://") ? "" : "http://") + address));
            return new HotelServiceProxy(httpEndpoint);
        }

        private void RegisterToZooKeeper()
        {
            InitializeAlliance();
            RegisterAsDelegate();    

            foreach (var hotel in Data.Values)
            {
                RegisterHotel(hotel, true);
            }
        }

        private void RegisterAsDelegate()
        {
            _log.Debug("Trying to register as delegate");
            try
            {
                Replicator.Create(string.Format(Replication.Delegate, Alliance), ServerData.ToByteArray(), Ids.OPEN_ACL_UNSAFE, CreateMode.Ephemeral);



                RegistrationProxy.Register(Alliance, DelegatePort);
            }
            catch (KeeperException.NodeExistsException)
            {
            }
        }

        private void RegisterHotel(Hotel hotel, bool shouldOverride)
        {
            _log.Debug("Trying to register hotel " + hotel.Name);
            var hotelPath = string.Format(Replication.HotelName, hotel.Alliance, hotel.Name);
            if (shouldOverride)
            {
                OverridePath(hotelPath, ServerData.ToByteArray(), CreateMode.Ephemeral);
            }
            else
            {
                try
                {
                    Replicator.Create(hotelPath, ServerData.ToByteArray(), Ids.OPEN_ACL_UNSAFE, CreateMode.Ephemeral);
                    _log.Debug("WatchBackupeeRecovered added to path: " + hotelPath);
                    var nodeData = Replicator.GetData(hotelPath, 
                        new WatchBackupeeRecovered(this, hotel.Name), 
                        new Stat()).ByteArrayToObject<Replication.ServerData>();
                }
                catch (KeeperException.NodeExistsException)
                {
                    var nodeData = Replicator.GetData(hotelPath, false, new Stat()).ByteArrayToObject<Replication.ServerData>();
                    if (!nodeData.Address.Equals(ServerData.Address))
                    {
                        Data.Remove(hotel.Name);
                    }
                }
            }
        }

        private void RemovePreviousServers(Hotel hotel)
        {
            try
            {
                foreach (var oldServer in Replicator.GetChildren(string.Format(Replication.HotelName, hotel.Alliance, hotel.Name), false))
                {
                    Replicator.Delete(string.Format(Replication.HotelServer, hotel.Alliance, hotel.Name, oldServer),-1);
                }
            }
            catch (KeeperException.NoNodeException)
            {}
        }

        private void InitializeAlliance()
        {
            _log.Debug("Initializing Alliance " + Alliance);
            InitializePath(Replication.Root, null, CreateMode.Persistent);
            InitializePath(string.Format(Replication.Cluster, Alliance), null, CreateMode.Persistent);
            InitializePath(string.Format(Replication.Hotels, Alliance), null, CreateMode.Persistent);
        }

        private void InitializePath(string path, byte[] data, CreateMode mode)
        {
            try
            {
                Replicator.Create(path, data, Ids.OPEN_ACL_UNSAFE, mode);
                _log.Debug("Initialize Path Success -> " + path);
            }
            catch (KeeperException.NodeExistsException)
            {
                _log.Debug("Initialize Path Failure -> " + path);
            }
        }

        private void OverridePath(string path, byte[] data, CreateMode mode)
        {
            try
            {
                Replicator.Create(path, data, Ids.OPEN_ACL_UNSAFE, mode);
                _log.Debug("Override Path Success Create -> " + path);
            }
            catch (KeeperException.NodeExistsException)
            {
                Replicator.SetData(path, data, -1);
                _log.Debug("Override Path Success Set Data -> " + path);
            }
            
        }

        public Hotel LoadData(string fileName)
        {
            var reader = new FileInfo(fileName).OpenText();
            string line = reader.ReadLine();
            if (line != null)
            {
                string[] items = line.Split(' ');

                if (items.Count() < 3)
                {
                    throw new InvalidDataException("invalid input file header");
                }
                var hotel = new Hotel
                {
                    Stars = double.Parse(items[0]),
                    Rating = double.Parse(items[1]),
                    Alliance = items[2],
                    Name = string.Join(" ", items.Skip(3)),
                    Rooms = new List<HotelRoom>(),
                    Reservations = new ConcurrentDictionary<int, HotelRoom>(),
                    BookingId = new Random().Next() % 1000
                };

                Console.WriteLine("alliance: " + hotel.Alliance + ", hotel: " + hotel.Name + ", stars: " + hotel.Stars + ", rating: " + hotel.Rating);

                HotelRoom room = null;
                Data[hotel.Name] = hotel;
                while ((line = reader.ReadLine()) != null)
                {
                    items = line.Split(' ');
                    if (items[0].Equals("Room"))
                    {
                        room = new HotelRoom(int.Parse(items[1]), double.Parse(items[2]));
                        hotel.Rooms.Add(room);
                    }
                    else
                    {
                        BookRoom(hotel.Name,DateTime.Parse(items[1]), DateTime.Parse(items[2]), room.RoomNumber);
                    }
                }
                return hotel;
            }
            return null;
        }

        public bool IsUp()
        {
            return true;
        }

        public Hotel GetAvailableRooms(string hotelName, DateTime fromDate, DateTime toDate, double minStars = 0, double minRating = 0)
        {
            var hotel = GetHotel(hotelName);

            if (hotel == null)
            {
                return null;
            }

            if (hotel.Stars < minStars || hotel.Rating < minRating)
            {
                _log.Debug("Hotel '" + hotelName + "' doesn't have the right stars or rating");
                return null;
            }

            _log.Debug("Sent available rooms from " + fromDate.ToShortDateString() + " to " + toDate.ToShortDateString());
            var result = new Hotel()
                       {
                           Name = hotel.Name,
                           Rating = hotel.Rating,
                           Stars = hotel.Stars,
                           Rooms = hotel.Rooms.Where(r => r.IsAvailable(fromDate, toDate)).ToList()
                       };
            return result.Rooms.Any() ? result : null;
        }

        public IList<Hotel> GetAvailableRooms(DateTime fromDate, DateTime toDate, double minStars, double minRating)
        {
            var result = new List<Hotel>();
            foreach (var hotelName in Replicator.GetChildren(string.Format(Replication.Hotels, Alliance), false))
	        {
                try
                {
                    Hotel hotel = null;
                    if (GetHotel(hotelName) != null)
                    {
                        hotel = GetAvailableRooms(hotelName, fromDate, toDate, minStars, minRating);
                        if (hotel == null)
                        {
                            continue;
                        }
                    }
                    if (hotel == null)
                    {
                        var server = Replicator.GetData(string.Format(Replication.HotelName, Alliance, hotelName), false, null).ByteArrayToObject<Replication.ServerData>();
                        var proxy = GenerateHotelServiceProxy(server.Address);
                        hotel = proxy.GetAvailableRooms(hotelName, fromDate, toDate, minStars, minRating);
                    }
                    if (hotel != null)
                    {
                        result.Add(hotel);
                        _log.Debug("Sent Available Rooms for hotel '" + hotel.Name + "'");
                    }
                }
                catch
                {
                    _log.Debug("Couldn't get hotel data.");
                }
	        }
            return result;
        }

        private int BookRoom(Hotel hotel, HotelRoom room, DateTime fromDate, DateTime toDate)
        {
            if (room == null || !hotel.Rooms.Any(r => r.RoomNumber.Equals(room.RoomNumber)))
            {
                throw new FaultException("Failed, no such room " + room.RoomNumber + " at " + hotel.Name);
            }
            room.Book(fromDate, toDate, hotel.BookingId);
            hotel.Reservations[hotel.BookingId] = room;
            _log.Debug("Booked room " + room.RoomNumber + " from " + fromDate.ToShortDateString() + " to " + toDate.ToShortDateString());
            _log.Debug("Booking Id: " + hotel.BookingId);
            return hotel.BookingId++;
        }

        public int BookRoom(string hotelName, DateTime fromDate, DateTime toDate, int roomNumber)
        {
            var hotel = GetHotel(hotelName);
            var room = hotel.Rooms.FirstOrDefault(r => r.RoomNumber == roomNumber);

            return BookRoom(hotel, room, fromDate, toDate);

        }


        public bool Cancel(string hotelName, int bookingId)
        {
            var hotel = GetHotel(hotelName);
            if (hotel.Reservations.ContainsKey(bookingId))
            {
                hotel.Rooms.Single(r => r.ReservedDates.ContainsKey(bookingId)).Unbook(bookingId);
                _log.Debug("Unbooked booking id " + bookingId);
                return true;
            }
            else
            {
                var msg = bookingId + " is an invalid booking id";
                _log.Error(msg);
                return false;
            }
        }

        private Hotel GetHotel(string hotelName)
        {
            if (Data.ContainsKey(hotelName))
                return Data[hotelName];
            else if (BackedUpData.ContainsKey(hotelName))
                return BackedUpData[hotelName];
            else
                return null;
        }

        public IEnumerable<string> GetHotelNames()
        {
            _log.Debug("Sent Hotels " + string.Join(",", Data.Keys));
            return Data.Keys;
        }

        public double GetStars(string hotelName)
        {
            return GetHotel(hotelName).Stars;
        }

        public double GetRating(string hotelName)
        {
            return GetHotel(hotelName).Rating;
        }

        public bool IsHotelInQueryRange(string hotelName, double minStars, double minRating)
        {
            return GetStars(hotelName) >= minStars && GetRating(hotelName) >= minRating;
        }

        public bool AddHotelToBackUp(IDictionary<string, Hotel> data)
        {
            try
            {
                foreach (var hotelData in data)
                {
                    BackedUpData[hotelData.Key] = hotelData.Value;
                    _log.Debug("WatchBackupee added for path: " + string.Format(Replication.HotelName, Alliance, hotelData.Key));
                    Replicator.GetData(string.Format(Replication.HotelName, Alliance, hotelData.Key),
                        new WatchBackupee(this, hotelData.Key),
                        new Stat());
                }
                RefreshServerData();
                return true;
            }
            catch
            {
                return false;
            }
        }

        private void RefreshServerData()
        {
            Replicator.SetData(string.Format(Replication.HotelName, Alliance, ServerData.MainHotelName),
                ServerData.ToByteArray(), -1);
            _log.Debug("Refreshed Server Data " + ServerData);
        }

        public void MoveFromBackupToData(string hotelName)
        {
            _log.Debug("Moving backup to active data for hotel " + hotelName);
            Data[hotelName] = BackedUpData[hotelName];
            BackedUpData.Remove(hotelName);
            RegisterHotel(Data[hotelName], false);
            RegisterAsDelegate();

            if (Data.ContainsKey(hotelName))
            {
                BackupData(Data[hotelName]);
            }
            RefreshServerData();
        }

    }
}
